#
# MediaDart, Copyright (C) 2006,2008 CRS4.
# All rights reserved.  Email: mediadart@crs4.it   Web: www.mediadart.org
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of:
#     The GNU Lesser General Public License as published by the Free
#     Software Foundation; either version 2.1 of the License, or (at
#     your option) any later version. The text of the GNU Lesser
#     General Public License is included with this library in the
#     file LICENSE.TXT.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the file
# LICENSE.TXT for more details.
#
# Derived from
# XML-RPC CLIENT LIBRARY
# xmlrpclib.py 65467 2008-08-04 00:50:11Z brett.cannon $
# Copyright (c) 1999-2002 by Secret Labs AB
# Copyright (c) 1999-2002 by Fredrik Lundh
#


#
#   This proxy does not support reading returned messages
#

from threading import RLock, Thread
from random import randint
from types import StringType, DictType
from uuid import uuid4
from json import dumps, loads
from amqplib import client_0_8 as amqp
from mediadart.mqueue import xmlspec
from mediadart.mqueue.mqserver import RPCError
from mediadart import log
from mediadart.utils import get_mq_params
from Queue import Queue

# --------------------------------------------------------------------
# request dispatcher


# The sigleton factory
_theProxyFactory = None
_aLock = RLock()  # to protect accesso to _theProxyFactory

class _Proxy:
    # some magic to bind an XML-RPC method to an RPC server.
    # supports "nested" methods (e.g. examples.getStateName)
    def __init__(self, factory, server_name, callback, userargs):
        """
          Create a proxy:

          @param factory: an instance of ProxyFactory
          @param server_name: the name of the class implementing the server
          @param callback: a function with signature (see below). If none calls
                    to this proxy are blocking.
          @param userargs: additional data to be passed to the callback

          The callback arguments must be None, a URL or a function.
          If <callback> is NULL the request is blocking and the result of the call is
          returned to teh caller.
          If <callback> is a URL the proxy just delivers the request message and returns
          immediately.
          If <callback> is a function the call is non
          blocking and when the response is received the callback function is invoked.
          The callback must have the signature (status_ok, result, userarg):
             status_ok: a boolean that is True if the remote call completed successfully, 
                        False otherwise. 
             result: if status_ok=True is the result of the call, an exception otherwise
             userarg: it is the value passed to the proxy at construction.
        """
        self.__factory = factory
        self.__server_name = server_name
        self.__userargs = userargs
        self.__method_name = ''
        self.__callback = callback
        self.__response_queue = None
        if callable(callback):
            self.__proxy_type = 'non-blocking'
        elif callback and callback.startswith('http://'):
            self.__proxy_type = 'request-only'
        else:
            self.__proxy_type = 'blocking'
            self.__response_queue = Queue()

        # Open a channel to the server for sending request messages
        factory._setup_target_queue(server_name)

    def __getattr__(self, name):
        #log.debug('_proxy.__getattr__: %s' % name)
        if self.__method_name:
            self.__method_name += ('.' + name)
        else:
            self.__method_name = name
        return self

    def __call__(self, *args, **kwargs):
        #log.debug("name=%s" % self.__method_name)
        method_name, self.__method_name = self.__method_name, ''
        if self.__proxy_type == 'blocking':
            def default_cb(status_ok, result, queue):
                queue.put((status_ok, result))
            t1 = Thread(target=self.__factory._request, args=(self.__server_name, method_name, default_cb, self.__response_queue, args, kwargs))
            t1.setDaemon(False)
            t1.start()
            status_ok, result = self.__response_queue.get()   # stops here for result
            if status_ok:
                return result
            elif isinstance(result, Exception):
                raise result
            else:
                raise RPCError('ERROR: %s' % result)
        else:
            self.__factory._request(self.__server_name, method_name, self.__callback, self.__userargs, args, kwargs)


##
# Standard server proxy.  This class establishes a virtual connection
# to an XML-RPC server.
# <p>
# This class is available as ServerProxy and Server.  New code should
# use ServerProxy, to avoid confusion.
#
# @def ServerProxy(uri, **options)
# @param uri The connection point on the server.
# @keyparam transport A transport factory, compatible with the
#    standard transport class.
# @keyparam encoding The default encoding used for 8-bit strings
#    (default is UTF-8).
# @keyparam verbose Use a true value to enable debugging output.
#    (printed to standard output).
# @see Transport

class ProxyFactory:
    """to [,options] -> a logical connection to an XML-RPC server

    uri the name of the class of the server to be invoked.

    The following options can be given at initialization as keyword arguments:
        host: the address of the  message broker.
        port: port of the message broker
        auth: a tuble ('username', 'password') to authenticate with the
        broker.

    Encoding of all strings is UTF-8. All strings passed to the server
    assume this encoding.
    """

    def __init__(self, host, port, username, password, vhost, rqname=''):
        self._conn = None
        self._reply_queue = None
        self._lock1 = RLock()   # used to protect access to _conn (class level)
        self._outstanding = {}  # id of msg to be waited for
        self._lock2 = RLock()    # used to protect accesses to _outstanding
        self._tag = 'mqrpc%02d' % randint(0, 99)
        self._wait_thread = None
        self._targets = []    # list if nome of queues already declared
        self.__allow_none = False
        self.__encoding = 'utf-8'
        self.rqname = rqname
        self.__connect(host, port, username, password, vhost)

    def __connect(self, host, port, user, password, vhost):
        with self._lock1:
            if self._conn:
                log.debug('reusing connection, channel etc.')
                pass
            else:
                log.debug('making connection')
                self._conn = amqp.Connection(host='%s:%s' % (host, port), 
                          userid=user, password=password, 
                          virtual_host=vhost, insist=False)
                self._chan = self._conn.channel()
                self._reply_queue, _, _ = self._chan.queue_declare(queue=self.rqname, auto_delete=True, exclusive=True)
                self._chan.exchange_declare(exchange="amq.direct", type="direct", durable=True)
                self._chan.queue_bind(queue=self._reply_queue, exchange="amq.direct", routing_key=self._reply_queue)
                self._chan.basic_consume(queue=self._reply_queue, no_ack=True, callback=self.__reply_callback, 
                               nowait=True, consumer_tag=self._tag)

#    def _setup(self, host, port, username, password, vhost):
#        self.__connect(host, port, username, password, vhost)
#        self._chan.basic_consume(queue=self._reply_queue, no_ack=True, callback=self.__reply_callback, 
#                       nowait=True, consumer_tag=self._tag)

    def _setup_target_queue(self, name):
        with self._lock1:
            if not name in self._targets:
                self._chan.queue_declare(queue=name, auto_delete=True, passive=True)
                self._chan.queue_bind(queue=name, exchange="amq.direct", routing_key=name)
                self._targets.append(name)

    def __reply_callback(self, msg):
        msg_id = getattr(msg, 'correlation_id', 'correlation id missing in message')
        process_message = 0
        user_cb = user_args = None
        with self._lock2:
            if msg_id in self._outstanding:
                process_message = 1
                user_cb, user_args = self._outstanding[msg_id]
                del self._outstanding[msg_id]
        if process_message:
            response = loads(msg.body)
            if response['version'] != '1.1':
                log.warning('response version is %s, supported is 1.1' % response['versione'])
            if response.get('result', None) is None:
                status_ok = False
                result = response['error']
            else:
                status_ok = True
                result = response['result']
            user_cb(status_ok, result, user_args)
        else:
            log.debug('unmatched message: %s' % msg_id)

    def _start_wait(self):
        self._wait_thread = Thread(target=self._do_wait)
        self._wait_thread.setDaemon(False)
        self._wait_thread.start()

    def _do_wait(self):
        while 1:
            self._chan.wait()
            #log.debug('exit wait: _outstanding =  %d' % len(self._outstanding))
            with self._lock2:
                if len(self._outstanding) == 0:
                    self._wait_thread = None
                    break
        #log.debug('_do_wait exit')

    def _request(self, servername, methodname, user_cb, user_args, params, kwparams):
        req_id = uuid4().get_hex()
        if not kwparams:
            p = params
        else:
            n = 0
            for p in params:
                kwparams[str(n)] = p
                n += 1
            p = kwparams
        req = {'method': methodname, 'params': p, 'version':'1.1', 'id':req_id}
        request = dumps(req)
        if hasattr(user_cb, 'startswith') and user_cb.startswith('http://'):
            reply_to = user_cb
        else:
            reply_to = self._reply_queue
        msg = amqp.Message(request, delivery_mode=1, reply_to=reply_to, correlation_id=req_id)
        if self._chan.connection is None:
            log.error('Unable to send message: internal data corruption')
            if user_cb:
                user_cb(False, 'Internal error: unable to send', user_args)
        else:
            with self._lock2:
                ret = self._chan.basic_publish(msg, mandatory=1, routing_key=servername)
                if reply_to == self._reply_queue:   # get response
                    self._outstanding[req_id] = (user_cb, user_args)
                    if not self._wait_thread:
                        self._start_wait()
                    else:
                        #log.debug('****not starting thread')
                        pass

    def __repr__(self):
        return "<ProxyFactory>"

    __str__ = __repr__


def _create_factory(broker_ip, broker_port, username, password, vhost):
    global _aLock, _theProxyFactory
    with _aLock:
        msg = 'Connecting to broker broker_ip=%s, broker_port=%s, username=%s, password=*****, vhost=%s' % (broker_ip, broker_port, username, vhost)
        if _theProxyFactory is None:
            log.debug(msg)
            _theProxyFactory = ProxyFactory(broker_ip, broker_port, username, password, vhost)
#            return ProxyFactory(broker_ip, broker_port, username, password, vhost)
        elif _theProxyFactory._chan.connection is None:
            log.error('Restarting corrupted Proxy singleton')
            log.debug(msg)
            rqname = _theProxyFactory.rqname
            _theProxyFactory = ProxyFactory(broker_ip, broker_port, username, password, vhost, rqname)
        else:
            pass # use existing singleton

def Proxy(server, callback='', userargs=None, host=None, port=None, username=None, password=None, vhost=None):
    global _theProxyFactory
    _create_factory(**get_mq_params(host, port, username, password, vhost))
    return _Proxy(_theProxyFactory, server, callback, userargs)
#    return _Proxy(proxy_factory, server, callback, userargs)
